﻿using System;
using System.Collections.Generic;
using System.Text;
using SSG.Tools;

namespace SSG.Model
{
    /// <summary>
    /// Content generating part
    /// </summary>
    public partial class Procedure
    {
        /// <summary>
        /// To ESE format, force implementation
        /// </summary>
        /// <returns></returns>
        public abstract string ToEse(GeneratorSettings settings = null);

        /// <summary>
        /// To ESE format.
        /// </summary>
        /// <returns></returns>
        protected string ToEse(string type, GeneratorSettings settings = null)
        {
            if (settings == null) settings = GeneratorSettings.DefaultSettings;
            if (settings == null) throw new InvalidOperationException("No default settings");

            StringBuilder sb = new StringBuilder();

            // Generate lines for each runway of the track
            foreach (Runway rw in this.Runways) {
                // Write fixes for each assigned runway
                List<Fix> blacklistedFixes;
                List<Fix> fixes = this.FixesBlacklistConsidered(settings, out blacklistedFixes, rw);

                // check if we have fixes
                StringBuilder comments = new StringBuilder(this.NavaidsInformation());
                if (fixes.NullSafeIsEmpty()) {
                    comments.AppendIfNotNullOrEmpty("no fixes ", Generator.SctEseCommentSeperator);
                    comments.Append(this.Name);
                    comments.Append(' ');
                    comments.Append(rw.Name);
                }
                else {
                    sb.Append(type);
                    sb.Append(this.Airport.Name.ToUpperInvariant());
                    sb.Append(':');
                    sb.Append(rw.Name.ToUpperInvariant());
                    sb.Append(':');
                    sb.Append(settings.TrackNameWithoutSpaces ? this.SmartNameNoSpaces : this.SmartName);
                    sb.Append(':');
                    bool startedReplaceComment = false;
                    for (int f = 0; f < fixes.Count; f++) {
                        Fix fix = fixes[f];
                        string name = fix.GetNameReplacedByRules(settings);
                        sb.Append(name);
                        // name replacement comments
                        if (fix.Name != name && settings.ExtendedComments) {
                            if (!startedReplaceComment) {
                                comments.AppendIfNotNullOrEmpty("repl. ", Generator.SctEseCommentSeperator);
                                comments.Append(fix.Name);
                                startedReplaceComment = true;
                            }
                            else {
                                comments.AppendIfNotNullOrEmpty(fix.Name, Generator.SctEseCommentSeperator);
                            }
                            comments.Append("->");
                            comments.Append(name);
                        }
                        if (f + 1 < fixes.Count) sb.Append(' ');
                    } // fixes
                    sb.AppendIfBuilderIsNotEmpty("\n");
                }

                // comment of blacklisted
                if (!blacklistedFixes.NullSafeIsEmpty()) {
                    comments.AppendIfNotNullOrEmpty("blacklisted: ", " ");
                    comments.Append(BaseEntity.GetNamesString(blacklistedFixes));
                }

                // build output
                if (settings.ExtendedComments && comments.Length > 0) {
                    sb.Append(Generator.SctEseOutputCommentExtendedSpace);
                    sb.Append('[');
                    sb.Append(this.Name);
                    sb.Append(' ');
                    sb.Append(rw.Name);
                    sb.Append("] ");
                    sb.Append(comments);
                    sb.Append('\n');
                }
            } // Runways

            return sb.ToString();
        }

        /// <summary>
        /// Convert to SCT format
        /// </summary>
        /// <param name="startPoint"></param>
        /// <param name="runway"></param>
        /// <param name="settings"></param>
        /// <param name="fixesConsidered"></param>
        /// <returns></returns>
        public string ToSct(out int fixesConsidered, string startPoint = null, Runway runway = null, GeneratorSettings settings = null)
        {
            if (settings == null) settings = GeneratorSettings.DefaultSettings;
            if (settings == null) throw new InvalidOperationException("No default settings");
            StringBuilder sb = new StringBuilder();
            List<Fix> blacklistedFixes;
            List<Fix> fixes = this.FixesBlacklistConsidered(settings, out blacklistedFixes, runway);
            fixesConsidered = fixes.NullSafeCount();

            // fixes for this runway?
            if (fixes.NullSafeIsEmpty()) {
                // There are SIDs with no "direct" fixes
                // KLAX CHATY2 has no direct fixes but only runway tracks
                // EDDT SIDs contains also olny runway tracks (Cycle 1212)
                if (settings.ExtendedComments) {
                    sb.Append(Generator.SctEseOutputCommentExtendedSpace + "No fixes for " + this.Name);
                    if (runway != null) {
                        sb.Append(' ');
                        sb.Append(runway.Name);
                    }
                    else {
                        sb.Append(". Check whether there are only runway tracks, not retrieved with no sort order.");
                    }
                    sb.Append('\n');
                    return sb.ToString();
                }
            }

            // ReSharper disable PossibleUnintendedReferenceComparison
            StringBuilder comments = new StringBuilder(this.NavaidsInformation());
            Setting order = settings.GetSctOrder(this);
            if (order == null) throw new InvalidOperationException("Missing order for " + this);
            // write Header comment
            if (order != Setting.OrderBySidStarTransition) {
                sb.Append(Generator.SctEseOutputCommentSpace);
                sb.Append(settings.TrackNameWithoutSpaces ? this.SmartNameNoSpaces : this.SmartName);
                sb.Append('\n');
            }
            // ReSharper restore PossibleUnintendedReferenceComparison

            int fixCount = fixes.Count;
            string colour = settings.GetSctColor(this.GetType());
            if (!String.IsNullOrEmpty(colour)) {
                string icao = (this.Airport != null) ? this.Airport.Name : null;
                Dictionary<string, string> replaceColourPlaceholders = new Dictionary<string, string>
                    {
                        {Generator.PlaceholderAirportIcao, icao},
                        {Generator.PlaceholderType, this.GetClassname()},
                        {Generator.PlaceholderFiletype, "sct"},
                        {Generator.PlaceholderTrackName, settings.TrackNameWithoutSpaces ? this.NameNoSpaces : this.NameSpaceToUnderscore},
                        {Generator.PlaceholderTrackSmartName, settings.TrackNameWithoutSpaces ? this.SmartNameNoSpaces : this.SmartNameSpaceToUnderscore}
                    };
                // more than 1 runway needs to be replaced in outer scope
                if (this.Runways.Count == 1) replaceColourPlaceholders.Add(Generator.PlaceholderRunway, this.Runways[0].Name);
                colour = Generator.ReplacePatternPlaceholders(colour, replaceColourPlaceholders);
                colour = colour.ToUpperInvariant().RemoveWhitespaces();
            }

            // startpoint if given
            if (!String.IsNullOrEmpty(startPoint) && fixCount > 0) {
                Procedure.AddSctFix(fixes[0], startPoint, colour, sb, comments, settings);
            }

            // add info for all fixes
            for (int fx = 1; fx < fixCount; fx++) {
                string from = fixes[fx - 1].GetNameReplacedByRules(settings).PadRight(Fix.SctCol);
                Procedure.AddSctFix(fixes[fx], from, colour, sb, comments, settings);
            }

            // comments, if any
            Procedure.AddBlacklistedFixesComments(blacklistedFixes, comments);
            if (settings.ExtendedComments && comments.Length > 0) {
                sb.Append(Generator.SctEseOutputCommentExtendedSpace);
                sb.Append('[');
                sb.Append(this.Name);
                if (runway != null) {
                    sb.Append(' ');
                    sb.Append(runway.Name);
                }
                sb.Append("] ");
                sb.Append(comments);
                sb.Append('\n');
            }

            // bye
            return sb.ToString();
        }

        /// <summary>
        /// Appends fixpoint to builder
        /// </summary>
        /// <param name="fixpoint"></param>
        /// <param name="from"></param>
        /// <param name="colour"></param>
        /// <param name="sb"></param>
        /// <param name="comments"></param>
        /// <param name="settings"></param>
        private static void AddSctFix(Fix fixpoint, string from, string colour, StringBuilder sb, StringBuilder comments, GeneratorSettings settings)
        {
            if (fixpoint == null) throw new ArgumentNullException("fixpoint");
            if (String.IsNullOrEmpty(from)) throw new ArgumentNullException("from");
            if (sb == null) throw new ArgumentNullException("sb");
            if (comments == null) throw new ArgumentNullException("comments");
            if (settings == null) throw new ArgumentNullException("settings");

            string toE = fixpoint.GetNameReplacedByRules(settings);
            if (toE != fixpoint.Name) {
                comments.AppendIfNotNullOrEmpty("repl. ", Generator.SctEseCommentSeperator);
                comments.Append(fixpoint.Name);
                comments.Append("->");
                comments.Append(toE);
            }
            string to = toE.PadRight(Fix.SctCol);
            if (!String.IsNullOrEmpty(colour)) toE = to;
            sb.Append(Fix.SctCol1Str);
            sb.Append(from);
            if (from.Length < Fix.SctCol + 1) sb.Append(from);
            sb.Append(to);
            sb.Append(toE);
            sb.AppendIfNotNullOrEmpty(colour);
            sb.Append('\n');
        }

        /// <summary>
        /// Kml output
        /// </summary>
        /// <param name="runway"></param>
        /// <param name="level"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public virtual string ToKml(Runway runway, int level, GeneratorSettings settings)
        {
            // Checks
            if (runway == null) throw new ArgumentNullException("runway");
            if (settings == null) throw new ArgumentNullException("settings");

            // Check for blacklisted fixes
            // TODO: Add KML "comment" / info about blacklisted fixes
            List<Fix> blacklistedFixes;
            List<Fix> fixes = this.FixesBlacklistConsidered(settings, out blacklistedFixes, runway);

            // Generate a route of the fixes
            if (runway.Altitude == null) throw new ApplicationException("Missing altitude for " + runway);
            Route route = new Route(fixes, false, this.Name + BaseEntity.CanonicalNameSeperator + runway.Name, "Calculated route") {DefaultAltitude = new Altitude(runway.Altitude), CorrespondingType = this.GetType()};

            if (this is Sid) {
                // start with runway for SID
                Runway rw = new Runway(runway);
                route.InsertFix(rw, false);
                Altitude transitionAlt = this.TransitionsMinAltitude;
                if (transitionAlt != null)
                    route.DefaultAltitude = transitionAlt;
                else
                    route.DefaultAltitude.ValueFt = route.DefaultAltitude.ValueFt + 5000;
            }
            else {
                Altitude transitionAlt = this.TransitionsMaxAltitude;
                if (transitionAlt != null)
                    route.DefaultAltitude = transitionAlt;
                else
                    route.DefaultAltitude.ValueFt = route.DefaultAltitude.ValueFt + 7000;
            }
            route.Calculate();

            // builders for the various KML content
            StringBuilder fixPlacemarks = new StringBuilder();
            StringBuilder fixesLineString = new StringBuilder();
            string ind1 = Kml.Indention(level + 1);
            string ind2 = Kml.Indention(level + 2);
            string ind3 = Kml.Indention(level + 3);

            // Sub tracks:
            // Recursive call of this method to add transitions
            if (!this.Transitions.NullSafeIsEmpty()) {
                StringBuilder transitions = new StringBuilder();
                foreach (Transition transition in this.Transitions) {
                    string trKml = transition.ToKml(runway, level + 1, settings);
                    trKml = Kml.KmlWrapInFolder(transition.Name + " " + this.GetClassname(), null, trKml, level + 1);
                    transitions.Append(trKml);
                }
                fixPlacemarks.Append(transitions);
            }

            // line string of all route fixes
            fixesLineString.Append(ind1);
            fixesLineString.Append("<Placemark>\n");
            fixesLineString.Append(this.Name.KmlName(level + 2, "\n"));
            fixesLineString.Append(false.KmlVisibility(level + 2, "\n"));
            fixesLineString.Append((Procedure.KmlTrackStyle + "." + this.GetClassname()).KmlStyleUrl(level + 2, "\n"));
            fixesLineString.Append(ind2);
            fixesLineString.Append("<LineString>\n");
            fixesLineString.Append(false.KmlVisibility(level + 3, "\n"));
            fixesLineString.Append(true.KmlExtrude(level + 3, "\n"));
            fixesLineString.Append(ind3);
            fixesLineString.Append("<altitudeMode>absolute</altitudeMode>\n");
            fixesLineString.Append(ind3);
            fixesLineString.Append("<coordinates>\n");

            // placemarks and line string for all fixes
            foreach (Fix f in route.Fixes) {
                // placemark
                fixPlacemarks.Append(f.ToKml(level + 1, settings));

                // line string
                Altitude alt = f.Altitude;
                fixesLineString.Append(Kml.Coordinates(f.Latitude, f.Longitude, alt == null ? 100 : alt.ValueM, level + 4, settings));
                fixesLineString.Append('\n');
            }

            // terminate lines string
            fixesLineString.Append(ind3);
            fixesLineString.Append("</coordinates>\n");
            fixesLineString.Append(ind2);
            fixesLineString.Append("</LineString>\n");
            fixesLineString.Append(ind1);
            fixesLineString.Append("</Placemark>\n");
            fixPlacemarks.Append(fixesLineString);

            // Legs
            StringBuilder legPlacemarks = new StringBuilder();
            foreach (Leg leg in route.Legs) {
                legPlacemarks.Append(leg.ToKml(level + 1, settings));
            }
            if (legPlacemarks.Length > 0) {
                fixPlacemarks.Append(Kml.KmlWrapInFolder("Legs " + this.Name + "/" + runway.Name, null, legPlacemarks.ToString(), level + 1));
            }

            // return
            return fixPlacemarks.ToString();
        }
    }
}